/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/animation.h"
#include "engine/app.h"
#include "engine/engine.h"
#include "engine/menu.h"
#include "engine/render.h"
#include "engine/sound.h"
#include "version.h"

/**************************************************************************
* Preprocessing
**************************************************************************/
#define MENU_BOUND_SIZE 100

/**************************************************************************
* Types
**************************************************************************/
struct menu_node;
typedef struct menu_node menu_node_type;
struct menu_node
{
  menu_type* menu;
  menu_node_type* next;
};

/**************************************************************************
* Variables
**************************************************************************/
const bounds_type MENU_BOUNDS = {
  -MENU_BOUND_SIZE, MENU_BOUND_SIZE, -MENU_BOUND_SIZE, MENU_BOUND_SIZE
};
menu_type menu_about;
bounds_type menu_bounds = {0, 10, 0, 10};
navigation_type menu_nav_forward = FORWARD;
navigation_type menu_nav_back = BACK;
navigation_type menu_nav_up = UP;
navigation_type menu_nav_down = DOWN;

/**************************************************************************
* Static Variables
**************************************************************************/
static const float ANIMATION_SECONDS = 0.5;

static bounds_type menu_title_bounds;
static bounds_type menu_options_bounds[MAX_MENU_OPTIONS + 1];
static bounds_type orig_menu_title_bounds;
static bounds_type orig_menu_options_bounds[MAX_MENU_OPTIONS + 1];

static menu_type* current_menu = NULL;
static menu_node_type* menu_history_stack = NULL;
static int option_vertices_offset = -1;
static const int OPTIONS_VERTEX_COUNT = 6;

/**************************************************************************
* Function Prototypes
**************************************************************************/
static BOOL activate_option(int option_idx);
static BOOL activate_option_at(float x, float y);
static BOOL menu_ready(void);
static void on_menu_action(action_func_type action_func, void** action_args);
static void option_next(menu_type* menu, int option_idx);
static void option_prev(menu_type* menu, int option_idx);
static void option_select(menu_type* menu, int option_idx);
static void render_about(void** args);
static void render_option(int option_idx, BOOL draw_background);
static BOOL valid_option_idx(int option_idx);

/**************************************************************************
* Function Implementations
**************************************************************************/
static BOOL activate_option(int option_idx)
{
  if (!valid_option_idx(option_idx)) {
    current_menu->active_option_idx = -1;
    return FALSE;
  }

  // play a sound if item changed
  if (current_menu->active_option_idx != option_idx) {
    sound_play(&sound_click, FALSE);
  }

  current_menu->active_option_idx = option_idx;
  return TRUE;
}

static BOOL activate_option_at(float x, float y)
{
  int i;

  if (!menu_ready()) {
    return FALSE;
  }

  for (i = 0; i <= MAX_MENU_OPTIONS; i++) {
    if (!valid_option_idx(i)) {
      continue;
    }
    if (bounds_contain(&(menu_options_bounds[i]), x, y)) {
      // the click hit a menu option, handle it
      return activate_option(i);
    }
  }

  current_menu->active_option_idx = -1;
  return FALSE;
}

BOOL menu_active(void)
{
  return (current_menu != NULL);
}

void menu_close(void)
{
  menu_node_type* menu_node;
  // empty stack
  while (menu_history_stack) {
    menu_node = menu_history_stack;
    menu_history_stack = menu_history_stack->next;
    free(menu_node);
  }
  current_menu = NULL;
}

void menu_goto(void** args)
{
  int i;
  float temp;
  menu_node_type* menu_node;
  navigation_type navigation;

  // set current menu
  if (args[0]) {
    navigation = (*((navigation_type*)args[0]));
  } else {
    navigation = FORWARD;
  }

  switch (navigation) {
  case BACK:
    if (!menu_history_stack) {
      return;
    } else {
      // pop the previous menu off the stack
      current_menu = menu_history_stack->menu;
      menu_node = menu_history_stack;
      menu_history_stack = menu_history_stack->next;
      free(menu_node);
    }
    break;
  case FORWARD:
    // push previous menu on stack
    if (current_menu) {
      menu_node = malloc(sizeof(menu_node_type));
      menu_node->menu = current_menu;
      menu_node->next = menu_history_stack;
      menu_history_stack = menu_node;
    }
    current_menu = (menu_type*)args[1];
    break;
  default:
    return;
  }

  if (current_menu->options_count > 0) {
    // set the first option to active
    current_menu->active_option_idx = 0;
  } else {
    // no options available
    current_menu->active_option_idx = MAX_MENU_OPTIONS;
  }

  // create animation tasks
  menu_title_bounds.bottom = MENU_BOUNDS.top;
  menu_title_bounds.top = (MENU_BOUNDS.top + (2.0 * bounds_height(&orig_menu_title_bounds)));
  animation_add(&(menu_title_bounds.bottom), orig_menu_title_bounds.bottom,
                ANIMATION_SECONDS);
  animation_add(&(menu_title_bounds.top), orig_menu_title_bounds.top,
                ANIMATION_SECONDS);

  temp = bounds_width(&(orig_menu_options_bounds[0]));
  for (i = 0; i < MAX_MENU_OPTIONS; i++) {
    switch (navigation) {
    case FORWARD:
      // fly in from left
      menu_options_bounds[i].left = (MENU_BOUNDS.left - (2 * temp));
      menu_options_bounds[i].right = MENU_BOUNDS.left;
      animation_add(&(menu_options_bounds[i].left),
                    orig_menu_options_bounds[i].left, ANIMATION_SECONDS);
      animation_add(&(menu_options_bounds[i].right),
                    orig_menu_options_bounds[i].right, ANIMATION_SECONDS);
      break;
    case BACK:
      // fly in from right
      menu_options_bounds[i].left = MENU_BOUNDS.right;
      menu_options_bounds[i].right = (MENU_BOUNDS.right + (2 * temp));
      animation_add(&(menu_options_bounds[i].left),
                    orig_menu_options_bounds[i].left, ANIMATION_SECONDS);
      animation_add(&(menu_options_bounds[i].right),
                    orig_menu_options_bounds[i].right, ANIMATION_SECONDS);
      break;
    default:
      // no animation
      break;
    }
  }
}

void menu_init(void)
{
  static const float OPTION_VERTICES[][3] = {
    {-0.6, 0, 0}, {-0.5, 0.5, 0}, {0.5, 0.5, 0},
    {0.6, 0, 0}, {0.5, -0.5, 0}, {-0.5, -0.5, 0},
  };
  static const float OPTION_TEXTURE[][3] = {
    {0, 0.5, 0}, {0, 1, 0}, {1, 1, 0},
    {1, 0.5, 0}, {1, 0, 0}, {0, 0, 0},
  };

  int i;
  float delta_y, y, width;

  // setup options' locations, skip some space for title and last option
  delta_y = (bounds_height(&MENU_BOUNDS) / (float)(3 + MAX_MENU_OPTIONS));

  menu_title_bounds = MENU_BOUNDS;
  menu_title_bounds.bottom = (menu_title_bounds.top - (1.5 * delta_y));
  orig_menu_title_bounds = menu_title_bounds;

  y = menu_title_bounds.bottom - (0.5 * delta_y);
  width = bounds_width(&MENU_BOUNDS);
  for (i = 0; i <= MAX_MENU_OPTIONS; i++) {
    menu_options_bounds[i].left = MENU_BOUNDS.left + (width / 6.0);
    menu_options_bounds[i].right = MENU_BOUNDS.right - (width / 6.0);
    menu_options_bounds[i].top = y;
    menu_options_bounds[i].bottom = y - delta_y;
    orig_menu_options_bounds[i] = menu_options_bounds[i];
    y -= delta_y;
  }

  menu_options_bounds[MAX_MENU_OPTIONS].top = MENU_BOUNDS.bottom + delta_y;
  menu_options_bounds[MAX_MENU_OPTIONS].bottom = MENU_BOUNDS.bottom;
  orig_menu_options_bounds[MAX_MENU_OPTIONS] = menu_options_bounds[MAX_MENU_OPTIONS];

  option_vertices_offset = render_add_vertices(OPTIONS_VERTEX_COUNT,
    (float*)OPTION_VERTICES, NULL, (float*)OPTION_TEXTURE, NULL);

  // setup about menu
  memset(&menu_about, 0, sizeof(menu_type));
  strcpy(menu_about.title, "About");
  menu_about.on_render = &render_about;
  strcpy(menu_about.options[MAX_MENU_OPTIONS].name, "Back");
  menu_about.options[MAX_MENU_OPTIONS].on_select = menu_goto;
  menu_about.options[MAX_MENU_OPTIONS].select_args[0] = &menu_nav_back;

  // setup the actual menus
  menu_setup();
}

void menu_navigate(navigation_type navigation)
{
  int new_idx;
  menu_option_type* active_option;

  if (!menu_ready()) {
    return;
  }

  active_option = &(current_menu->options[current_menu->active_option_idx]);

  // handle the navigation
  switch (navigation) {
  case FORWARD:
    option_select(current_menu, current_menu->active_option_idx);
    break;
  case BACK:
    option_select(current_menu, MAX_MENU_OPTIONS);
    break;
  case LEFT:
    option_prev(current_menu, current_menu->active_option_idx);
    break;
  case RIGHT:
    option_next(current_menu, current_menu->active_option_idx);
    break;
  case UP:
    new_idx = (current_menu->active_option_idx - 1);
    if (new_idx < 0) {
      // to back menu item
      new_idx = MAX_MENU_OPTIONS;
    } else if (new_idx >= current_menu->options_count) {
      if (current_menu->options_count > 0) {
        // back to last item
        new_idx = (current_menu->options_count - 1);
      } else {
        // only the back option
        new_idx = MAX_MENU_OPTIONS;
      }
    }
    activate_option(new_idx);
    break;
  case DOWN:
    new_idx = (current_menu->active_option_idx + 1);
    if (new_idx > MAX_MENU_OPTIONS) {
      if (new_idx > 0) {
        new_idx = 0;
      } else {
        new_idx = MAX_MENU_OPTIONS;
      }
    } else if (new_idx >= current_menu->options_count) {
      // to back menu item
      new_idx = MAX_MENU_OPTIONS;
    }
    activate_option(new_idx);
    break;
  default:
    break;
  }

  // play menu sounds
  switch (navigation) {
  case FORWARD:
  case BACK:
    sound_play(&sound_bubble, FALSE);
    break;
  case LEFT:
  case RIGHT:
    sound_play(&sound_click, FALSE);
    break;
  default:
    break;
  }
}

void menu_on_close(void** args)
{
  UNUSED_PARAM(args);
  menu_close();
}

void menu_on_quit(void** args)
{
  UNUSED_PARAM(args);
  engine_quit(TRUE, 0);
}

static BOOL menu_ready(void)
{
  return (current_menu && !animation_pending());
}

void menu_render(const bounds_type* bounds)
{
  int i;
  bounds_type content_bounds;
  bounds_type temp_bounds;
  color_type temp_color;

  if (!menu_active()) {
    return;
  }

  // keep aspect ratio of menu
  menu_bounds = MENU_BOUNDS;
  bounds_keep_aspect(bounds, &menu_bounds);

  glPushMatrix();
  render_set_view_ortho(&menu_bounds);
 
  // render the title reflection
  temp_bounds = menu_title_bounds;
  temp_bounds.top -= (2 * bounds_height(&temp_bounds));
  bounds_slide(&temp_bounds, 0, (-0.25 * bounds_height(&temp_bounds)));
  temp_color = RED;
  temp_color.a = 48;
  render_set_color(&temp_color);
  render_button(&temp_bounds, current_menu->title, NULL, NULL);

  // render the title
  render_set_color(&RED);
  render_button(&menu_title_bounds, current_menu->title, NULL, NULL);

  // render options
  for (i = 0; i < current_menu->options_count; i++) {
    render_option(i, TRUE);
  }

  if (current_menu->on_render) {
    // custom rendering
    content_bounds.top = menu_options_bounds[current_menu->options_count].top;
    content_bounds.bottom = menu_options_bounds[MAX_MENU_OPTIONS].top;
    content_bounds.left = MENU_BOUNDS.left;
    content_bounds.right = MENU_BOUNDS.right;

    bounds_shrink(&content_bounds, 5.0);

    current_menu->render_args[0] = &content_bounds;
    current_menu->on_render(current_menu->render_args);
  }

  // last option is 'back' option
  render_option(MAX_MENU_OPTIONS, FALSE);
  glPopMatrix();
}

void menu_toggle_option(navigation_type navigation, const char* option_desc,
                        menu_option_type* option, BOOL* option_value)
{
  switch (navigation) {
  case DOWN:
  case UP:
    _TOGGLE(*option_value);
    break;
  default:
    break;
  }

  strcpy(option->name, option_desc);
  if (*option_value) {
    strcat(option->name, ": On");
  } else {
    strcat(option->name, ": Off");
  }

  app_settings_save();
}

void menu_touch_move(const touch_type* touch)
{
  activate_option_at(touch->locs[0][X], touch->locs[0][Y]);
}

void menu_touch_start(const touch_type* touch)
{
  activate_option_at(touch->locs[0][X], touch->locs[0][Y]);
}

void menu_touch_stop(const touch_type* touch)
{
  float x_center, y_center;

  if (!menu_ready()) {
    return;
  }

  if (!valid_option_idx(current_menu->active_option_idx)) {
    return;
  }

  if (current_menu->options[current_menu->active_option_idx].on_select) {
    menu_navigate(FORWARD);
  } else {
    // handle prev/next
    bounds_center(&(menu_options_bounds[current_menu->active_option_idx]),
      &x_center, &y_center);
    if (touch->locs[0][X] >= x_center) {
      menu_navigate(RIGHT);
    } else {
      menu_navigate(LEFT);
    }
  }
}

void menu_update(float secs)
{
  UNUSED_PARAM(secs);
}

static void on_menu_action(action_func_type action_func, void** action_args)
{
  if (!action_func) {
    return;
  }

  // start animation and activate menu action
  action_func(action_args);
}

static void option_next(menu_type* menu, int option_idx)
{
  on_menu_action(menu->options[option_idx].on_next,
                 menu->options[option_idx].next_args);
}

static void option_prev(menu_type* menu, int option_idx)
{
  on_menu_action(menu->options[option_idx].on_prev,
                 menu->options[option_idx].prev_args);
}

static void option_select(menu_type* menu, int option_idx)
{
  if (menu->options[option_idx].on_select) {
    on_menu_action(menu->options[option_idx].on_select,
                   menu->options[option_idx].select_args);
  } else {
    // no select action, try to use next action
    on_menu_action(menu->options[option_idx].on_next,
                   menu->options[option_idx].next_args);
  }
}

static void render_about(void** args)
{
  float width, height;
  bounds_type* bounds = (bounds_type*)args[0];

  width = bounds_width(bounds);
  height = bounds_width(bounds);

  render_set_color(&OVERLAY_COLOR);
  render_rect(bounds, TRUE);
  render_set_color(&(ACTIVE_TEXT_COLORS[FALSE]));
  render_rect(bounds, FALSE);
  font_render_text_wrapped(APP_ABOUT, (height / 15.0),
                           bounds->left, bounds->top, width);
  font_render_text(APP_COPYRIGHT, (height / 18.0),
                   bounds_center_x(bounds), (bounds->bottom + (height / 30.0)));
  font_render_text(APP_VERSION, height / 20.0, (bounds->right - (width / 8.0)),
                   (bounds->bottom - (height / 40.0)));
}

static void render_option(int option_idx, BOOL draw_background)
{
  bounds_type* option_bounds;

  if (!current_menu) {
    return;
  }

  option_bounds = &(menu_options_bounds[option_idx]);

  if (draw_background) {
    glPushMatrix();
    glTranslatef(bounds_center_x(option_bounds),
                 bounds_center_y(option_bounds), 0);
    glScalef(bounds_width(option_bounds),
             bounds_height(option_bounds) / 1.25, 1);

    render_set_color(&OVERLAY_COLOR);

    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, render_button_texture);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glDrawArrays(GL_TRIANGLE_FAN, option_vertices_offset, OPTIONS_VERTEX_COUNT);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);

    glDisable(GL_TEXTURE_2D);

    glPopMatrix();
  }

  // render the text
  render_set_color(&(ACTIVE_TEXT_COLORS[option_idx == current_menu->active_option_idx]));
  render_button(option_bounds, current_menu->options[option_idx].name, NULL, NULL);
}

static BOOL valid_option_idx(int option_idx)
{
  if (!menu_ready()) {
    return FALSE;
  } else {
    // there is a current menu
    return ((option_idx == MAX_MENU_OPTIONS) ||
            ((option_idx >= 0) &&
             (option_idx < current_menu->options_count)));
  }
}
